package gateway;

import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
//import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
//import org.springframework.cloud.context.config.annotation.RefreshScope;
//import org.springframework.cloud.gateway.config.GatewayProperties;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.cloud.gateway.filter.ratelimit.RedisRateLimiter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
//import org.springframework.context.annotation.Primary;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationManager;
import org.springframework.security.oauth2.provider.token.RemoteTokenServices;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;

import java.net.InetSocketAddress;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Function;

//import org.springframework.cloud.gateway.route.RouteLocator;
//import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;

@Configuration
@ComponentScan
@RestController
@EnableCircuitBreaker//enabled actuator '/hystrix.stream' for turbine aggregator
@EnableDiscoveryClient
@SpringBootApplication
@EnableAutoConfiguration
public class GatewayProxyApplication {

    @RequestMapping("/hystrixfallback")
    public Map hystrixfallback(@RequestParam(required = false) String dependency) {
        Map result = new LinkedHashMap();

        result.put("state", "offline");
        result.put("dependency", dependency);
        result.put("provider", "fallback@Gateway");

        return result;
    }

    @Bean
    KeyResolver remoteAddressKeyResolver() {
        return exchange -> {
            InetSocketAddress remoteAddress = exchange.getRequest().getRemoteAddress();
            return Mono.just(remoteAddress.getHostString() + ":" + remoteAddress.getPort());
        };
    }

//    @Bean
//    @Primary
//    @RefreshScope
//    @ConfigurationProperties("spring.cloud.gateway")
//    public GatewayProperties gatewayProperties() {
//        return new GatewayProperties();
//    }
//    @Bean
//    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
//
//        return builder.routes()
//                .route("SERVICE-SET-1", r ->
//                        r.path("/SERVICE-SET-1/**")
//                                .filters(f ->
//                                        f.hystrix(c -> c.setName("DemoHystrixCmd").setFallbackUri("forward:/hystrixfallback"))
//                                )
//                                .uri("lb://SERVICE-SET-1")
//                )
//                .route("SERVICE-SET-2", r ->
//                        r.path("/SERVICE-SET-2/**")
//                                .filters(f ->
//                                                f.requestRateLimiter(c -> c.setRateLimiter(redisRateLimiter()))
//                                                        .hystrix(c -> c.setName("DemoHystrixCmd").setFallbackUri("forward:/hystrixfallback"))
//                                )
//                                .uri("lb://SERVICE-SET-2")
//                )
//                .route("SERVICE-SET-3", r ->
//                        r.path("/SERVICE-SET-3/**").uri("lb://SERVICE-SET-3")
//                )
//                .build();
//    }

    /**
     * https://github.com/spring-cloud/spring-cloud-gateway/issues/179#issuecomment-406238177
     */
    @Configuration
    protected class OAuth2Configurer {

        @Bean("oauthAuthenticationWebFilter")
        public AuthenticationWebFilter oauthAuthenticationWebFilter(OAuth2AuthenticationManagerAdapter authManager, OAuthTokenConverter tokenConverter) {
            AuthenticationWebFilter filter = new AuthenticationWebFilter(authManager);
            filter.setServerAuthenticationConverter(tokenConverter::apply);
            return filter;
        }

        @Bean
        public ResourceServerTokenServices tokenService() {
            RemoteTokenServices tokenServices = new RemoteTokenServices();
            tokenServices.setClientId("acme");
            tokenServices.setClientSecret("acmesecret");
            tokenServices.setCheckTokenEndpointUrl("http://localhost:8080/oauth/check_token");
            return tokenServices;
        }
    }

    @Component
    protected class OAuth2AuthenticationManagerAdapter implements ReactiveAuthenticationManager {

        private final AuthenticationManager authenticationManager;

        @Autowired
        public OAuth2AuthenticationManagerAdapter(ResourceServerTokenServices tokenServices) {
            this.authenticationManager = oauthManager(tokenServices);
        }

        public Mono<Authentication> authenticate(Authentication token) {
            return Mono.just(token).publishOn(Schedulers.elastic()).flatMap(t -> {
                try {
                    return Mono.just(this.authenticationManager.authenticate(t));
                } catch (Exception x) {
                    return Mono.error(new BadCredentialsException("Invalid or expired access token presented"));
                }
            }).filter(Authentication::isAuthenticated);
        }

        private AuthenticationManager oauthManager(ResourceServerTokenServices tokenServices) {
            OAuth2AuthenticationManager oauthAuthenticationManager = new OAuth2AuthenticationManager();
            oauthAuthenticationManager.setResourceId("");
            oauthAuthenticationManager.setTokenServices(tokenServices);
            return oauthAuthenticationManager;
        }

    }

    @Component
    protected class OAuthTokenConverter implements Function<ServerWebExchange, Mono<Authentication>> {

        private static final String BEARER = "bearer ";

        @Override
        public Mono<Authentication> apply(ServerWebExchange exchange) {
            String token = extractToken(exchange.getRequest());
            if (token != null) {
                return Mono.just(new PreAuthenticatedAuthenticationToken(token, ""));
            }
            return Mono.empty();
        }

        private String extractToken(ServerHttpRequest request) {
            String token = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
            if (StringUtils.isBlank(token) || !token.toLowerCase().startsWith(BEARER)) {
                return null;
            }
            return token.substring(BEARER.length());
        }

    }

    /**
     * The configuration defined here is what really drives the edge service.
     * Any request that doesn't get handled by the other two configs will be
     * handled by this one. All requests coming through here must have a
     * valid access token
     */
    @Bean
    SecurityWebFilterChain oauthTokenAuthConfig(ServerHttpSecurity security, AuthenticationWebFilter oauthAuthenticationWebFilter) {

        return security
                .csrf().disable()
                .logout().disable()
                .httpBasic().disable()
                .formLogin().disable()
                .exceptionHandling().and()
                .securityMatcher(notMatches("/unsecuredRoutes/**"))
                .addFilterAt(oauthAuthenticationWebFilter, SecurityWebFiltersOrder.HTTP_BASIC)
                .authorizeExchange().anyExchange().authenticated()
                .and().build();
    }

    private ServerWebExchangeMatcher matches(String... routes) {
        return ServerWebExchangeMatchers.pathMatchers(routes);
    }

    private ServerWebExchangeMatcher notMatches(String... routes) {
        return new NegatedServerWebExchangeMatcher(matches(routes));
    }

    /*
    https://github.com/spring-projects/spring-security/blob/5.1.0.M2/web/src/main/java/org/springframework/security/web/server/util/matcher/NegatedServerWebExchangeMatcher.java
     */
    protected class NegatedServerWebExchangeMatcher implements ServerWebExchangeMatcher {
        private final ServerWebExchangeMatcher matcher;

        public NegatedServerWebExchangeMatcher(ServerWebExchangeMatcher matcher) {
            Assert.notNull(matcher, "matcher cannot be null");
            this.matcher = matcher;
        }

        /* (non-Javadoc)
         * @see org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher#matches(org.springframework.web.server.ServerWebExchange)
         */
        @Override
        public Mono<MatchResult> matches(ServerWebExchange exchange) {
            return matcher.matches(exchange)
                    .flatMap(m -> m.isMatch() ? MatchResult.notMatch() : MatchResult.match());
        }

        @Override
        public String toString() {
            return "NegatedServerWebExchangeMatcher{" + "matcher=" + matcher + "}";
        }
    }

    public static void main(String[] args) {
        SpringApplication.run(GatewayProxyApplication.class, args);
    }
}
